//by thakis

#include "gcvid.h"

#include <cstdlib> //NULL
#include <cstring> //memcmp
#include <string>
#include <cassert>
using namespace std;

void readThpHeader(FILE* f, ThpHeader& h)
{
  fread(&h, sizeof(h), 1, f);
  toDWORD(h.version);  
  toDWORD(h.maxBufferSize);
  toDWORD(h.maxAudioSamples);
  toFLOAT(h.fps);
  toDWORD(h.numFrames);
  toDWORD(h.firstFrameSize);
  toDWORD(h.dataSize);
  toDWORD(h.componentDataOffset);
  toDWORD(h.offsetsDataOffset);
  toDWORD(h.firstFrameOffset);
  toDWORD(h.lastFrameOffset);
}

void readThpComponents(FILE* f, ThpComponents& c)
{
  fread(&c, sizeof(c), 1, f);
  toDWORD(c.numComponents);
}

void readThpVideoInfo(FILE* f, ThpVideoInfo& i, bool isVersion11)
{
  fread(&i, sizeof(i), 1, f);
  toDWORD(i.width);
  toDWORD(i.height);
  if(isVersion11)
    toDWORD(i.unknown);
  else
  {
    i.unknown = 0;
    fseek(f, -4, SEEK_CUR);
  }
}

void readThpAudioInfo(FILE* f, ThpAudioInfo& i, bool isVersion11)
{
  fread(&i, sizeof(i), 1, f);
  toDWORD(i.numChannels);
  toDWORD(i.frequency);
  toDWORD(i.numSamples);
  if(isVersion11)
    toDWORD(i.numData);
  else
  {
    i.numData = 1;
    fseek(f, -4, SEEK_CUR);
  }
}

void readMthHeader(FILE* f, MthHeader& h)
{
  fread(&h, sizeof(h), 1, f);
  toDWORD(h.unknown);  
  toDWORD(h.unknown2);
  toDWORD(h.unknown3);

  toDWORD(h.width);
  toDWORD(h.height);
  toDWORD(h.unknown4);
  toDWORD(h.numFrames);

  toDWORD(h.offset);
  toDWORD(h.unknown5);
  toDWORD(h.firstFrameSize);
}


struct DecStruct
{
  const u8* currSrcByte;
  u32 blockCount;
  u8 index;
  u8 shift;
};

void thpAudioInitialize(DecStruct& s, const u8* srcStart)
{
  s.currSrcByte = srcStart;
  s.blockCount = 2;
  s.index = (*s.currSrcByte >> 4) & 0x7;
  s.shift = *s.currSrcByte & 0xf;
  ++s.currSrcByte;
}

s32 thpAudioGetNewSample(DecStruct& s)
{
  //the following if is executed all 14 calls
  //to thpAudioGetNewSample() (once for each
  //microblock) because mask & 0xf can contain
  //16 different values and starts with 2
  if((s.blockCount & 0xf) == 0)
  {
    s.index = (*s.currSrcByte >> 4) & 0x7;
    s.shift = *s.currSrcByte & 0xf;
    ++s.currSrcByte;
    s.blockCount += 2;
  }

  s32 ret;
  if((s.blockCount & 1) != 0)
  {
    s32 t = (*s.currSrcByte  << 28) & 0xf0000000;
    ret = t >> 28; //this has to be an arithmetic shift
    ++s.currSrcByte;
  }
  else
  {
    s32 t = (*s.currSrcByte << 24) & 0xf0000000;
    ret = t >> 28; //this has to be an arithmetic shift
  }

  ++s.blockCount;
  return ret;
}

int thpAudioDecode(s16* destBuffer, const u8* srcBuffer, bool separateChannelsInOutput, bool isInputStereo)
{
  if(destBuffer == NULL || srcBuffer == NULL)
    return 0;

  ThpAudioBlockHeader* head = (ThpAudioBlockHeader*)srcBuffer;

  u32 channelInSize = aDWORD(head->channelSize);
  u32 numSamples = aDWORD(head->numSamples);

  const u8* srcChannel1 = srcBuffer + sizeof(ThpAudioBlockHeader);
  const u8* srcChannel2 = srcChannel1 + channelInSize;

  s16* table1 = head->table1;
  s16* table2 = head->table2;

  s16* destChannel1, * destChannel2;
  u32 delta;

  if(separateChannelsInOutput)
  {
    //separated channels in output
    destChannel1 = destBuffer;
    destChannel2 = destBuffer + numSamples;
    delta = 1;
  }
  else
  {
    //interleaved channels in output
    destChannel1 = destBuffer;
    destChannel2 = destBuffer + 1;
    delta = 2;
  }

  DecStruct s;
  if(!isInputStereo)
  {
    //mono channel in input

    thpAudioInitialize(s, srcChannel1);

    s16 prev1 = aSHORT(*(s16*)(srcBuffer + 72));
    s16 prev2 = aSHORT(*(s16*)(srcBuffer + 74));
    
    for(int i = 0; i < numSamples; ++i)
    {
      s64 res = (s64)thpAudioGetNewSample(s);
      res = ((res << s.shift) << 11); //convert to 53.11 fixedpoint

      //these values are 53.11 fixed point numbers
      s64 val1 = aSHORT(table1[2*s.index]);
      s64 val2 = aSHORT(table1[2*s.index + 1]);

      //convert to 48.16 fixed point
      res = (val1*prev1 + val2*prev2 + res) << 5;

      //rounding:
      u16 decimalPlaces = res & 0xffff;
      if(decimalPlaces > 0x8000) //i.e. > 0.5
        //round up
        ++res;
      else if(decimalPlaces == 0x8000) //i.e. == 0.5
        if((res & 0x10000) != 0)
          //round up every other number
          ++res;

      //get nonfractional parts of number, clamp to [-32768, 32767]
      s32 final = (res >> 16);
      if(final > 32767) final = 32767;
      else if(final < -32768) final = -32768;

      prev2 = prev1;
      prev1 = final;
      *destChannel1 = (s16)final;
      *destChannel2 = (s16)final;
      destChannel1 += delta;
      destChannel2 += delta;
    }
  }
  else
  {
    //two channels in input - nearly the same as for one channel,
    //so no comments here (different lines are marked with XXX)

    thpAudioInitialize(s, srcChannel1);
    s16 prev1 = aSHORT(*(s16*)(srcBuffer + 72));
    s16 prev2 = aSHORT(*(s16*)(srcBuffer + 74));
    for(int i = 0; i < numSamples; ++i)
    {
      s64 res = (s64)thpAudioGetNewSample(s);
      res = ((res << s.shift) << 11);
      s64 val1 = aSHORT(table1[2*s.index]);
      s64 val2 = aSHORT(table1[2*s.index + 1]);
      res = (val1*prev1 + val2*prev2 + res) << 5;
      u16 decimalPlaces = res & 0xffff;
      if(decimalPlaces > 0x8000)
        ++res;
      else if(decimalPlaces == 0x8000)
        if((res & 0x10000) != 0)
          ++res;
      s32 final = (res >> 16);
      if(final > 32767) final = 32767;
      else if(final < -32768) final = -32768;
      prev2 = prev1;
      prev1 = final;
      *destChannel1 = (s16)final;
      destChannel1 += delta;
    }

    thpAudioInitialize(s, srcChannel2);//XXX
    prev1 = aSHORT(*(s16*)(srcBuffer + 76));//XXX
    prev2 = aSHORT(*(s16*)(srcBuffer + 78));//XXX
    for(int j = 0; j < numSamples; ++j)
    {
      s64 res = (s64)thpAudioGetNewSample(s);
      res = ((res << s.shift) << 11);
      s64 val1 = aSHORT(table2[2*s.index]);//XXX
      s64 val2 = aSHORT(table2[2*s.index + 1]);//XXX
      res = (val1*prev1 + val2*prev2 + res) << 5;
      u16 decimalPlaces = res & 0xffff;
      if(decimalPlaces > 0x8000)
        ++res;
      else if(decimalPlaces == 0x8000)
        if((res & 0x10000) != 0)
          ++res;
      s32 final = (res >> 16);
      if(final > 32767) final = 32767;
      else if(final < -32768) final = -32768;
      prev2 = prev1;
      prev1 = final;
      *destChannel2 = (s16)final;
      destChannel2 += delta;
    }
  }

  return numSamples;
}


VideoFrame::VideoFrame()
: _data(NULL), _w(0), _h(0), _p(0)
{}

VideoFrame::~VideoFrame()
{ dealloc(); }

void VideoFrame::resize(int width, int height)
{
  if(width == _w && height == _h)
    return;

  dealloc();
  _w = width;
  _h = height;

  //24 bpp, 4 byte padding
  _p = 3*width;
  _p += (4 - _p%4)%4;

  _data = new u8[_p*_h];
}

int VideoFrame::getWidth() const
{ return _w; }

int VideoFrame::getHeight() const
{ return _h; }

int VideoFrame::getPitch() const
{ return _p; }

u8* VideoFrame::getData()
{ return _data; }

const u8* VideoFrame::getData() const
{ return _data; }

void VideoFrame::dealloc()
{
  if(_data != NULL)
    delete [] _data;
  _data = NULL;
  _w = _h = _p = 0;
}

//swaps red and blue channel of a video frame
void swapRB(VideoFrame& f)
{
  u8* currLine = f.getData();

  int hyt = f.getHeight();
  int pitch = f.getPitch();

  for(int y = 0; y < hyt; ++y)
  {
    for(int x = 0, x2 = 2; x < pitch; x += 3, x2 += 3)
    {
      u8 t = currLine[x];
      currLine[x] = currLine[x2];
      currLine[x2] = t;
    }
    currLine += pitch;
  }
}


enum FILETYPE
{
  THP, MTH, JPG,
    UNKNOWN = -1
};

FILETYPE getFiletype(FILE* f)
{
  long t = ftell(f);
  fseek(f, 0, SEEK_SET);

  u8 buff[4];
  fread(buff, 1, 4, f);

  FILETYPE ret = UNKNOWN;
  if(memcmp("THP\0", buff, 4) == 0)
    ret = THP;
  else if(memcmp("MTHP", buff, 4) == 0)
    ret = MTH;
  else if(buff[0] == 0xff && buff[1] == 0xd8)
    ret = JPG;

  fseek(f, t, SEEK_SET);
  return ret;
}

long getFilesize(FILE* f)
{
  long t = ftell(f);
  fseek(f, 0, SEEK_END);
  long ret = ftell(f);
  fseek(f, t, SEEK_SET);
  return ret;
}

void decodeJpeg(const u8* data, int size, VideoFrame& dest);


VideoFile::VideoFile(FILE* f)
: _f(f)
{}

VideoFile::~VideoFile()
{
  if(_f != NULL)
    fclose(_f);
  _f = NULL;
}

int VideoFile::getWidth() const
{ return 0; }

int VideoFile::getHeight() const
{ return 0; }

float VideoFile::getFps() const
{ return 0.f; }

int VideoFile::getFrameCount() const
{ return 0; }

int VideoFile::getCurrentFrameNr() const
{ return 0; }

void VideoFile::loadNextFrame()
{}

void VideoFile::getCurrentFrame(VideoFrame& f) const
{}

bool VideoFile::hasSound() const
{ return false; }

int VideoFile::getFrequency() const
{ return 0; }

int VideoFile::getMaxAudioSamples() const
{ return 0; }

int VideoFile::getCurrentBuffer(s16* data) const
{ return 0; }

void VideoFile::loadFrame(VideoFrame& frame, const u8* data, int size) const
{
  decodeJpeg(data, size, frame);
}


ThpVideoFile::ThpVideoFile(FILE* f)
: VideoFile(f)
{
  readThpHeader(f, _head);

  //this is just to find files that have this field != 0, i
  //have no such a file
  assert(_head.offsetsDataOffset == 0);

  readThpComponents(f, _components);
  for(int i = 0; i < _components.numComponents; ++i)
  {
    if(_components.componentTypes[i] == 0) //video
      readThpVideoInfo(_f, _videoInfo, _head.version == 0x00011000);
    else if(_components.componentTypes[i] == 1) //audio
    {
      readThpAudioInfo(_f, _audioInfo, _head.version == 0x00011000);
      assert(_head.maxAudioSamples != 0);
    }
  }

  _numInts = 3;
  if(_head.maxAudioSamples != 0)
    _numInts = 4;

  _currFrameNr = -1;
  _nextFrameOffset = _head.firstFrameOffset;
  _nextFrameSize = _head.firstFrameSize;
  _currFrameData.resize(_head.maxBufferSize); //include some padding
  loadNextFrame();
}

int ThpVideoFile::getWidth() const
{ return _videoInfo.width; }

int ThpVideoFile::getHeight() const
{ return _videoInfo.height; }

float ThpVideoFile::getFps() const
{ return _head.fps; }

int ThpVideoFile::getFrameCount() const
{ return _head.numFrames; }

int ThpVideoFile::getCurrentFrameNr() const
{ return _currFrameNr; }

void ThpVideoFile::loadNextFrame()
{
  ++_currFrameNr;
  if(_currFrameNr >= _head.numFrames)
  {
    _currFrameNr = 0;
    _nextFrameOffset = _head.firstFrameOffset;
    _nextFrameSize = _head.firstFrameSize;
  }

  fseek(_f, _nextFrameOffset, SEEK_SET);
  fread(&_currFrameData[0], 1, _nextFrameSize, _f);

  _nextFrameOffset += _nextFrameSize;
  _nextFrameSize = aDWORD(*(u32*)&_currFrameData[0]);
}

void ThpVideoFile::getCurrentFrame(VideoFrame& f) const
{
  int size = aDWORD(*(u32*)(&_currFrameData[0] + 8));
  loadFrame(f, &_currFrameData[0] + 4*_numInts, size);
}

bool ThpVideoFile::hasSound() const
{ return _head.maxAudioSamples != 0; }

int ThpVideoFile::getFrequency() const
{
  if(hasSound())
    return _audioInfo.frequency;
  else
    return 0;
}

int ThpVideoFile::getMaxAudioSamples() const
{ return _head.maxAudioSamples; }

int ThpVideoFile::getCurrentBuffer(s16* data) const
{
  if(!hasSound())
    return 0;

  int jpegSize = aDWORD(*(u32*)(&_currFrameData[0] + 8));
  const u8* src = &_currFrameData[0] + _numInts*4 + jpegSize;

  return thpAudioDecode(data, src, false, _audioInfo.numChannels == 2);
}

MthVideoFile::MthVideoFile(FILE* f)
: VideoFile(f)
{
  readMthHeader(f, _head);

  _currFrameNr = -1;
  _nextFrameOffset = _head.offset;
  _nextFrameSize = _head.firstFrameSize;
  _thisFrameSize = 0;

  _currFrameData.resize(_head.unknown3);
}


int MthVideoFile::getWidth() const
{ return _head.width; }

int MthVideoFile::getHeight() const
{ return _head.height; }

float MthVideoFile::getFps() const
{
  return 50.f; //TODO: This has to be in there somewhere
}

int MthVideoFile::getFrameCount() const
{
  return _head.numFrames;
}

int MthVideoFile::getCurrentFrameNr() const
{ return _currFrameNr; }

void MthVideoFile::loadNextFrame()
{
  ++_currFrameNr;
  if(_currFrameNr >= _head.numFrames)
  {
    _currFrameNr = 0;
    _nextFrameOffset = _head.offset;
    _nextFrameSize = _head.firstFrameSize;
  }

  fseek(_f, _nextFrameOffset, SEEK_SET);
  _currFrameData.resize(_nextFrameSize);
  fread(&_currFrameData[0], 1, _nextFrameSize, _f);
  _thisFrameSize = _nextFrameSize;

  u32 nextSize;
  nextSize = aDWORD(*(u32*)(&_currFrameData[0]));
  _nextFrameOffset += _nextFrameSize;
  _nextFrameSize = nextSize;

}

void MthVideoFile::getCurrentFrame(VideoFrame& f) const
{
  int size = _thisFrameSize;
  loadFrame(f, &_currFrameData[0] + 4, size - 4);
}


JpgVideoFile::JpgVideoFile(FILE* f)
: VideoFile(f)
{
  vector<u8> data(getFilesize(f));
  fread(&data[0], 1, getFilesize(f), f);

  loadFrame(_currFrame, &data[0], getFilesize(f));
}

int JpgVideoFile::getWidth() const
{ return _currFrame.getWidth(); }

int JpgVideoFile::getHeight() const
{ return _currFrame.getHeight(); }

int JpgVideoFile::getFrameCount() const
{ return 1; }

void JpgVideoFile::getCurrentFrame(VideoFrame& f) const
{
  f.resize(_currFrame.getWidth(), _currFrame.getHeight());
  memcpy(f.getData(), _currFrame.getData(),f.getPitch()*f.getHeight());
}

VideoFile* openVideo(const string& fileName)
{
  FILE* f = fopen(fileName.c_str(), "rb");
  if(f == NULL)
    return NULL;

  FILETYPE type = getFiletype(f);
  switch(type)
  {
    case THP:
      return new ThpVideoFile(f);
    case MTH:
      return new MthVideoFile(f);
    case JPG:
      return new JpgVideoFile(f);

    default:
      fclose(f);
      return NULL;
  }
}

void closeVideo(VideoFile*& vf)
{
  if(vf != NULL)
    delete vf;
  vf = NULL;
}

//as mentioned above, we have to convert 0xff to 0xff 0x00
//after the image date has begun (ie, after the 0xff 0xda marker)
//but we must not convert the end-of-image-marker (0xff 0xd9)
//this way. There may be 0xff 0xd9 bytes embedded in the image
//data though, so I add 4 bytes to the input buffer
//and fill them with zeroes and check for 0xff 0xd9 0 0
//as end-of-image marker. this is not correct, but works
//and is easier to code... ;-)
//a better solution would be to patch jpeglib so that this conversion
//is not neccessary

u8 endBytesThp[] = { 0xff, 0xd9, 0, 0 }; //used in thp files
u8 endBytesMth[] = { 0xff, 0xd9, 0xff, 0 }; //used in mth files

int countRequiredSize(const u8* data, int size, int& start, int& end)
{
  start = 2*size;
  int count = 0;

  int j;
  for(j = size - 1; data[j] == 0; --j)
    ; //search end of data

  if(data[j] == 0xd9) //thp file
    end = j - 1;
  else if(data[j] == 0xff) //mth file
    end = j - 2;

  for(int i = 0; i < end; ++i)
  {
    if(data[i] == 0xff)
    {
      //if i == srcSize - 1, then this would normally overrun src - that's why 4 padding
      //bytes are included at the end of src
      if(data[i + 1] == 0xda && start == 2*size)
        start = i;
      if(i > start)
        ++count;
    }
  }
  return size + count;
}

void convertToRealJpeg(u8* dest, const u8* src, int srcSize, int start, int end)
{
  int di = 0;
  for(int i = 0; i < srcSize; ++i, ++di)
  {
    dest[di] = src[i];
    //if i == srcSize - 1, then this would normally overrun src - that's why 4 padding
    //bytes are included at the end of src
    if(src[i] == 0xff && i > start && i < end)
    {
      ++di;
      dest[di] = 0;
    }
  }
}

void decodeRealJpeg(const u8* data, int size, VideoFrame& dest);

void decodeJpeg(const u8* data, int size, VideoFrame& dest)
{
  //convert format so jpeglib understands it...
  int start, end;
  int newSize = countRequiredSize(data, size, start, end);
  u8* buff = new u8[newSize];
  convertToRealJpeg(buff, data, size, start, end);

  //...and feed it to jpeglib
  decodeRealJpeg(buff, newSize, dest);

  delete [] buff;
}

extern "C"
{
#include "jpeglib.h"
#include <setjmp.h>
}

//the following functions are needed to let
//libjpeg read from memory instead of from a file...
//it's a little clumsy to do :-|
const u8* g_jpegBuffer;
int g_jpegSize;
bool g_isLoading = false;

void jpegInitSource(j_decompress_ptr cinfo)
{}

boolean jpegFillInputBuffer(j_decompress_ptr cinfo)
{
  cinfo->src->next_input_byte = g_jpegBuffer;
  cinfo->src->bytes_in_buffer = g_jpegSize;
  return TRUE;
}

void jpegSkipInputData(j_decompress_ptr cinfo, long num_bytes)
{
  cinfo->src->next_input_byte += num_bytes;
  cinfo->src->bytes_in_buffer -= num_bytes;
}

boolean jpegResyncToRestart(j_decompress_ptr cinfo, int desired)
{
  jpeg_resync_to_restart(cinfo, desired);
  return TRUE;
}

void jpegTermSource(j_decompress_ptr cinfo)
{}

void jpegErrorHandler(j_common_ptr cinfo)
{
  int a = 5;
  char buff[1024];
  (*cinfo->err->format_message)(cinfo, buff);
  //MessageBox(g_hWnd, buff, "JpegLib error:", MB_OK);
}

void decodeRealJpeg(const u8* data, int size, VideoFrame& dest)
{
  if(g_isLoading)
    return;
  g_isLoading = true;

  /*
  //debug
  FILE* fout = fopen("curr.jpg", "wb");
  fwrite(data, size, 1, fout);
  fclose(fout);
  //*/

  //decompressor state
  jpeg_decompress_struct cinfo;
  jpeg_error_mgr errorMgr;

  //read from memory manager
  jpeg_source_mgr sourceMgr;

  cinfo.err = jpeg_std_error(&errorMgr);
  errorMgr.error_exit = jpegErrorHandler;

  jpeg_create_decompress(&cinfo);

  //setup read-from-memory
  g_jpegBuffer = data;
  g_jpegSize = size;
  sourceMgr.bytes_in_buffer = size;
  sourceMgr.next_input_byte = data;
  sourceMgr.init_source = jpegInitSource;
  sourceMgr.fill_input_buffer = jpegFillInputBuffer;
  sourceMgr.skip_input_data = jpegSkipInputData;
  sourceMgr.resync_to_restart = jpegResyncToRestart;
  sourceMgr.term_source = jpegTermSource;
  cinfo.src = &sourceMgr;

  jpeg_read_header(&cinfo, TRUE);

#if 1
  //set quality/speed parameters to speed:
  cinfo.do_fancy_upsampling = FALSE;
  cinfo.do_block_smoothing = FALSE;

  //this actually slows decoding down:
  //cinfo.dct_method = JDCT_FASTEST;
#endif

  jpeg_start_decompress(&cinfo);

  dest.resize(cinfo.output_width, cinfo.output_height);

  if(cinfo.num_components == 3)
  {
    int y = 0;
    while(cinfo.output_scanline < cinfo.output_height)
    {
      //invert image because windows wants it downside up
      u8* destBuffer =  &dest.getData()[(dest.getHeight() - y - 1)*dest.getPitch()];

      //NO idea why jpeglib wants a pointer to a pointer
      jpeg_read_scanlines(&cinfo, &destBuffer, 1);
      ++y;
    }

    //jpeglib gives an error in jpeg_finish_decompress() if no all
    //scanlines are read by the application... :-|
    //(but because we read all scanlines, it's not really needed)
    cinfo.output_scanline = cinfo.output_height;

  }
  else
  {
    //MessageBox(g_hWnd, "Only RGB videos are currently supported.", "oops?", MB_OK);
  }

  jpeg_finish_decompress(&cinfo);
  jpeg_destroy_decompress(&cinfo);
  g_isLoading = false;
}
